/*******************************************************************************
 * Copyright (c) 2007, Geophysical Center RAS
 * Sergey Berezin, Dmitry Voitsekhovskiy
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * - Neither the name of the Geophysical Center RAS nor the names
 * of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml.Serialization;
using System.Xml;
using System.Runtime.CompilerServices;

namespace VisualEsse.DataEngine
{
    /// <summary>
    /// The XmlBaseCache class is a cache with custom keys with context stored in a separate XML file.
    /// </summary>
    public class XmlBasedCache : ICache
    {
        private static XmlBasedCache instance = null;

        public static XmlBasedCache GetSingleInstance(string folder, int maxSize)
        {
            if (instance == null)
            {
                instance = new XmlBasedCache(folder, maxSize);
            }
            return instance;
        }

        protected Dictionary<CacheKey, CacheFile> files = new Dictionary<CacheKey, CacheFile>();

        /// <summary>Full path to the cache folder</summary>
        protected string folder;

        /// <summary>Gets full path to the cache folder</summary>
        public string Folder
        {
            get
            {
                return folder;
            }
        }

        /// <summary>Maximum size of cache (in bytes)</summary>
        protected long maxSize;
        /// <summary>Current size of cache (in bytes)</summary>
        protected long currSize;

        /// <summary>
        /// Initializes a new instance of the XmlBasedCache class.
        /// </summary>
        /// <param name="folder">Full path to the cache folder.</param>
        /// <param name="maxSize">Maximum size of cache (in bytes).</param>
        public XmlBasedCache(string folder, int maxSize)
        {
            this.folder = folder;
            this.maxSize = maxSize;
            if (!Directory.Exists(folder))
                Directory.CreateDirectory(folder);
            if (!File.Exists(folder + "\\cache.xml"))
                this.currSize = 0;
            else
            {
                try
                {
                    LoadXml(folder + "\\cache.xml");
                }
                catch { this.currSize = 0; }
            }
        }

        public void StoreXml()
        {
            StoreXml(folder + "\\" + "cache.xml");
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        protected void LoadXml(string path)
        {
            XmlSerializer ks = new XmlSerializer(typeof(CacheKey));
            XmlSerializer fs = new XmlSerializer(typeof(CacheFile));
            XmlTextReader reader = new XmlTextReader(new StreamReader(path));
            while (reader.ReadToFollowing("file"))
            {
                reader.ReadToDescendant("CacheKey");
                CacheKey key = (CacheKey)ks.Deserialize(reader);
                CacheFile file = (CacheFile)fs.Deserialize(reader);
                files.Add(key, file);
                currSize += file.Size;
            }
            if (currSize > maxSize)
                CompressCache();
            reader.Close();
        }

        protected void StoreXml(string path)
        {
            XmlSerializer ks = new XmlSerializer(typeof(CacheKey));
            XmlSerializer fs = new XmlSerializer(typeof(CacheFile));
            XmlTextWriter writer = new XmlTextWriter(path, System.Text.Encoding.Default);
            writer.Formatting = Formatting.Indented;
            writer.WriteStartDocument();
            writer.WriteStartElement("cache");
            foreach (KeyValuePair<CacheKey, CacheFile> de in files)
            {
                writer.WriteStartElement("file");
                ks.Serialize(writer, de.Key);
                fs.Serialize(writer, de.Value);
                writer.WriteEndElement();
            }
            writer.WriteEndElement();
            writer.Close();
        }

        public Stream CreateFile(CacheKey key, FileAccess access)
        {
            lock (files)
            {
                try
                {
                    if (files.ContainsKey(key))
                    {
                        return new CacheStream(this, key, FileMode.Truncate, access);
                    }
                }
                catch { }

                string fileName = GetRandomFileName();
                files.Add(key, new CacheFile(fileName));
                return new CacheStream(this, key, FileMode.Create, access);
            }
        }

        public Stream OpenFile(CacheKey key, FileAccess access)
        {
            lock (files)
            {
                if (!files.ContainsKey(key))
                    return null;
                else
                    try
                    {
                        return new CacheStream(this, key, FileMode.Open, access);
                    }
                    catch/*(FileNotFoundException)*/
                    {
                        File.Delete(files[key].Name);
                        files.Remove(key);
                    }
                return null;
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public Stream OpenOrCreateFile(CacheKey key, FileAccess access)
        {            
            Stream stream = OpenFile(key, access);
            if (stream != null)
                return stream;

            return CreateFile(key, access);
        }

        public void UpdateFile(CacheKey key, long newLength)
        {
            lock (files)
            {
                CacheFile cf = GetCacheFile(key, true);
                currSize -= cf.Size;
                cf.Size = newLength;
                currSize += newLength;
                if (currSize > maxSize)
                    CompressCache();
            }
        }

        public void CompressCache()
        {
            lock (files)
            {
                Trace("Compressing cache from {0} bytes to {1}...", currSize, maxSize);
                int maxRounds = files.Count;
                int round = 0;
                while (currSize > maxSize && round < maxRounds)
                {
                    KeyValuePair<CacheKey, CacheFile> lru = new KeyValuePair<CacheKey, CacheFile>();
                    foreach (KeyValuePair<CacheKey, CacheFile> de in files)
                        if (lru.Value == null || lru.Value.AccessTime > de.Value.AccessTime)
                            lru = de;
                    if (lru.Value == null)
                        break; // No entries left
                    Trace("Removing entry {0}, access time = {1}, size = {2}",
                        lru.Value.Name, lru.Value.AccessTime, lru.Value.Size);
                    try
                    {
                        File.Delete(GetFullPath(lru.Value.Name));
                        currSize -= lru.Value.Size;
                        files.Remove(lru.Key);
                    }
                    catch
                    {
                        // File is in use. We do not remove it from cache.
                    }
                    round++;
                }
            }
        }

        public string GetRandomFileName()
        {
            string fileName = Path.GetRandomFileName();
            while (File.Exists(folder + "\\" + fileName))
                fileName = Path.GetRandomFileName();
            return fileName;
        }

        public string GetFullPath(string name)
        {
            return folder + "\\" + name;
        }

        public string GetFileName(CacheKey key, bool updateTime)
        {
            return GetFullPath(GetCacheFile(key, true).Name);
        }

        protected CacheFile GetCacheFile(CacheKey key, bool updateTime)
        {
            lock (files)
            {
                if (files.ContainsKey(key))
                {
                    CacheFile cf = files[key];
                    if (updateTime)
                        cf.UpdateAccessTime();
                    return cf;
                }
                else
                    throw new ArgumentException("Cache doesn't contains the key");
            }
        }

        protected void Trace(string fmt, params object[] args)
        {
            System.Diagnostics.Trace.WriteLine(String.Format(fmt, args), "Cache");
        }


        bool disposed = false;
        public void Dispose()
        {
            if (disposed)
                return;
            disposed = true;

            this.StoreXml();
        }
    }
}
